{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Question Answering with Langchain, AnalyticDB and OpenAI\n",
    "This notebook presents how to implement a Question Answering system with Langchain, AnalyticDB as a knowledge based and OpenAI embeddings. If you are not familiar with AnalyticDB, it’s better to check out the [Getting_started_with_AnalyticDB_and_OpenAI.ipynb](Getting_started_with_AnalyticDB_and_OpenAI.ipynb) notebook.\n",
    "\n",
    "This notebook presents an end-to-end process of:\n",
    "- Calculating the embeddings with OpenAI API.\n",
    "- Storing the embeddings in an AnalyticDB instance to build a knowledge base.\n",
    "- Converting raw text query to an embedding with OpenAI API.\n",
    "- Using AnalyticDB to perform the nearest neighbour search in the created collection to find some context.\n",
    "- Asking LLM to find the answer in a given context.\n",
    "\n",
    "All the steps will be simplified to calling some corresponding Langchain methods."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Prerequisites\n",
    "For the purposes of this exercise we need to prepare a couple of things:\n",
    "[AnalyticDB cloud instance](https://www.alibabacloud.com/help/en/analyticdb-for-postgresql/latest/product-introduction-overview).\n",
    "[Langchain](https://github.com/hwchase17/langchain) as a framework.\n",
    "An OpenAI API key."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Install requirements\n",
    "This notebook requires the following Python packages: `openai`, `tiktoken`, `langchain` and `psycopg2cffi`.\n",
    "- `openai` provides convenient access to the OpenAI API.\n",
    "- `tiktoken` is a fast BPE tokeniser for use with OpenAI's models.\n",
    "- `langchain` helps us to build applications with LLM more easily.\n",
    "- `psycopg2cffi` library is used to interact with the vector database, but any other PostgreSQL client library is also acceptable."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-05-06T10:21:40.843630Z",
     "start_time": "2023-05-06T10:21:38.796769Z"
    }
   },
   "outputs": [],
   "source": [
    "! pip install openai tiktoken langchain psycopg2cffi "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-05-06T10:21:40.974668Z",
     "start_time": "2023-05-06T10:21:40.845980Z"
    }
   },
   "outputs": [],
   "source": [
    "! export OPENAI_API_KEY=\"your API key\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-05-06T10:21:40.975073Z",
     "start_time": "2023-05-06T10:21:40.971906Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "OPENAI_API_KEY is ready\n"
     ]
    }
   ],
   "source": [
    "# Test that your OpenAI API key is correctly set as an environment variable\n",
    "# Note. if you run this notebook locally, you will need to reload your terminal and the notebook for the env variables to be live.\n",
    "import os\n",
    "\n",
    "# Note. alternatively you can set a temporary env variable like this:\n",
    "# os.environ[\"OPENAI_API_KEY\"] = \"sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n",
    "\n",
    "if os.getenv(\"OPENAI_API_KEY\") is not None:\n",
    "    print(\"OPENAI_API_KEY is ready\")\n",
    "else:\n",
    "    print(\"OPENAI_API_KEY environment variable not found\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Prepare your OpenAI API key\n",
    "The OpenAI API key is used for vectorization of the documents and queries.\n",
    "\n",
    "If you don't have an OpenAI API key, you can get one from [https://platform.openai.com/account/api-keys ).\n",
    "\n",
    "Once you get your key, please add it to your environment variables as `OPENAI_API_KEY` by running following command:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Prepare your AnalyticDB connection string\n",
    "To build the AnalyticDB connection string, you need to have the following parameters: `PG_HOST`, `PG_PORT`, `PG_DATABASE`, `PG_USER`, and `PG_PASSWORD`. You need to export them first to set correct connect string. Then build the connection string."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-05-06T10:21:41.574807Z",
     "start_time": "2023-05-06T10:21:40.976664Z"
    }
   },
   "outputs": [],
   "source": [
    "! export PG_HOST=\"your AnalyticDB host url\"\n",
    "! export PG_PORT=5432 # Optional, default value is 5432\n",
    "! export PG_DATABASE=postgres # Optional, default value is postgres\n",
    "! export PG_USER=\"your username\"\n",
    "! export PG_PASSWORD=\"your password\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "from langchain.vectorstores.analyticdb import AnalyticDB\n",
    "\n",
    "CONNECTION_STRING = AnalyticDB.connection_string_from_db_params(\n",
    "    driver=os.environ.get(\"PG_DRIVER\", \"psycopg2cffi\"),\n",
    "    host=os.environ.get(\"PG_HOST\", \"localhost\"),\n",
    "    port=int(os.environ.get(\"PG_PORT\", \"5432\")),\n",
    "    database=os.environ.get(\"PG_DATABASE\", \"postgres\"),\n",
    "    user=os.environ.get(\"PG_USER\", \"postgres\"),\n",
    "    password=os.environ.get(\"PG_PASSWORD\", \"postgres\"),\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "\n",
    "with open(\"questions.json\", \"r\") as fp:\n",
    "    questions = json.load(fp)\n",
    "\n",
    "with open(\"answers.json\", \"r\") as fp:\n",
    "    answers = json.load(fp)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Load data\n",
    "In this section we are going to load the data containing some natural questions and answers to them. All the data will be used to create a Langchain application with AnalyticDB being the knowledge base."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "when is the last episode of season 8 of the walking dead\n"
     ]
    }
   ],
   "source": [
    "print(questions[0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import wget\n",
    "\n",
    "# All the examples come from https://ai.google.com/research/NaturalQuestions\n",
    "# This is a sample of the training set that we download and extract for some\n",
    "# further processing.\n",
    "wget.download(\"https://storage.googleapis.com/dataset-natural-questions/questions.json\")\n",
    "wget.download(\"https://storage.googleapis.com/dataset-natural-questions/answers.json\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "No . overall No. in season Title Directed by Written by Original air date U.S. viewers ( millions ) 100 `` Mercy '' Greg Nicotero Scott M. Gimple October 22 , 2017 ( 2017 - 10 - 22 ) 11.44 Rick , Maggie , and Ezekiel rally their communities together to take down Negan . Gregory attempts to have the Hilltop residents side with Negan , but they all firmly stand behind Maggie . The group attacks the Sanctuary , taking down its fences and flooding the compound with walkers . With the Sanctuary defaced , everyone leaves except Gabriel , who reluctantly stays to save Gregory , but is left behind when Gregory abandons him . Surrounded by walkers , Gabriel hides in a trailer , where he is trapped inside with Negan . 101 `` The Damned '' Rosemary Rodriguez Matthew Negrete & Channing Powell October 29 , 2017 ( 2017 - 10 - 29 ) 8.92 Rick 's forces split into separate parties to attack several of the Saviors ' outposts , during which many members of the group are killed ; Eric is critically injured and rushed away by Aaron . Jesus stops Tara and Morgan from executing a group of surrendered Saviors . While clearing an outpost with Daryl , Rick is confronted and held at gunpoint by Morales , a survivor he met in the initial Atlanta camp , who is now with the Saviors . 102 `` Monsters '' Greg Nicotero Matthew Negrete & Channing Powell November 5 , 2017 ( 2017 - 11 - 05 ) 8.52 Daryl finds Morales threatening Rick and kills him ; the duo then pursue a group of Saviors who are transporting weapons to another outpost . Gregory returns to Hilltop , and after a heated argument , Maggie ultimately allows him back in the community . Eric dies from his injuries , leaving Aaron distraught . Despite Tara and Morgan 's objections , Jesus leads the group of surrendered Saviors to Hilltop . Ezekiel 's group attacks another Savior compound , during which several Kingdommers are shot while protecting Ezekiel . 103 `` Some Guy '' Dan Liu David Leslie Johnson November 12 , 2017 ( 2017 - 11 - 12 ) 8.69 Ezekiel 's group is overwhelmed by the Saviors , who kill all of them except for Ezekiel himself and Jerry . Carol clears the inside of the compound , killing all but two Saviors , who almost escape but are eventually caught by Rick and Daryl . En route to the Kingdom , Ezekiel , Jerry , and Carol are surrounded by walkers , but Shiva sacrifices herself to save them . The trio returns to the Kingdom , where Ezekiel 's confidence in himself as a leader has diminished . 104 5 `` The Big Scary U '' Michael E. Satrazemis Story by : Scott M. Gimple & David Leslie Johnson & Angela Kang Teleplay by : David Leslie Johnson & Angela Kang November 19 , 2017 ( 2017 - 11 - 19 ) 7.85 After confessing their sins to each other , Gabriel and Negan manage to escape from the trailer . Simon and the other lieutenants grow suspicious of each other , knowing that Rick 's forces must have inside information . The workers in the Sanctuary become increasingly frustrated with their living conditions , and a riot nearly ensues , until Negan returns and restores order . Gabriel is locked in a cell , where Eugene discovers him sick and suffering . Meanwhile , Rick and Daryl argue over how to take out the Saviors , leading Daryl to abandon Rick . 105 6 `` The King , the Widow , and Rick '' John Polson Angela Kang & Corey Reed November 26 , 2017 ( 2017 - 11 - 26 ) 8.28 Rick visits Jadis in hopes of convincing her to turn against Negan ; Jadis refuses , and locks Rick in a shipping container . Carl encounters Siddiq in the woods and recruits him to Alexandria . Daryl and Tara plot to deviate from Rick 's plans by destroying the Sanctuary . Ezekiel isolates himself at the Kingdom , where Carol tries to encourage him to be the leader his people need . Maggie has the group of captured Saviors placed in a holding area and forces Gregory to join them as punishment for betraying Hilltop . 106 7 `` Time for After '' Larry Teng Matthew Negrete & Corey Reed December 3 , 2017 ( 2017 - 12 - 03 ) 7.47 After learning of Dwight 's association with Rick 's group , Eugene affirms his loyalty to Negan and outlines a plan to get rid of the walkers surrounding the Sanctuary . With help from Morgan and Tara , Daryl drives a truck through the Sanctuary 's walls , flooding its interior with walkers , killing many Saviors . Rick finally convinces Jadis and the Scavengers to align with him , and they plan to force the Saviors to surrender . However , when they arrive at the Sanctuary , Rick is horrified to see the breached walls and no sign of the walker herd . 107 8 `` How It 's Gotta Be '' Michael E. Satrazemis David Leslie Johnson & Angela Kang December 10 , 2017 ( 2017 - 12 - 10 ) 7.89 Eugene 's plan allows the Saviors to escape , and separately , the Saviors waylay the Alexandria , Hilltop , and Kingdom forces . The Scavengers abandon Rick , after which he returns to Alexandria . Ezekiel ensures that the Kingdom residents are able to escape before locking himself in the community with the Saviors . Eugene aids Gabriel and Doctor Carson in escaping the Sanctuary in order to ease his conscience . Negan attacks Alexandria , but Carl devises a plan to allow the Alexandria residents to escape into the sewers . Carl reveals he was bitten by a walker while escorting Siddiq to Alexandria . 108 9 `` Honor '' Greg Nicotero Matthew Negrete & Channing Powell February 25 , 2018 ( 2018 - 02 - 25 ) 8.28 After the Saviors leave Alexandria , the survivors make for the Hilltop while Rick and Michonne stay behind to say their final goodbyes to a dying Carl , who pleads with Rick to build a better future alongside the Saviors before killing himself . In the Kingdom , Morgan and Carol launch a rescue mission for Ezekiel . Although they are successful and retake the Kingdom , the Saviors ' lieutenant Gavin is killed by Benjamin 's vengeful brother Henry . 109 10 `` The Lost and the Plunderers '' TBA TBA March 4 , 2018 ( 2018 - 03 - 04 ) TBD 110 11 `` Dead or Alive Or '' TBA TBA March 11 , 2018 ( 2018 - 03 - 11 ) TBD 111 12 `` The Key '' TBA TBA March 18 , 2018 ( 2018 - 03 - 18 ) TBD\n"
     ]
    }
   ],
   "source": [
    "print(answers[0])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Chain definition\n",
    "\n",
    "Langchain is already integrated with AnalyticDB and performs all the indexing for given list of documents. In our case we are going to store the set of answers we have."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.vectorstores import AnalyticDB\n",
    "from langchain.embeddings import OpenAIEmbeddings\n",
    "from langchain import VectorDBQA, OpenAI\n",
    "\n",
    "embeddings = OpenAIEmbeddings()\n",
    "doc_store = AnalyticDB.from_texts(\n",
    "    texts=answers, embedding=embeddings, connection_string=CONNECTION_STRING,\n",
    "    pre_delete_collection=True,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "At this stage all the possible answers are already stored in  AnalyticDB, so we can define the whole QA chain."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.chains import RetrievalQA\n",
    "\n",
    "llm = OpenAI()\n",
    "qa = VectorDBQA.from_chain_type(\n",
    "    llm=llm,\n",
    "    chain_type=\"stuff\",\n",
    "    vectorstore=doc_store,\n",
    "    return_source_documents=False,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Search data\n",
    "\n",
    "Once the data is put into AnalyticDB we can start asking some questions. A question will be automatically vectorized by OpenAI model, and the created vector will be used to find some possibly matching answers in AnalyticDB. Once retrieved, the most similar answers will be incorporated into the prompt sent to OpenAI Large Language Model.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "import random\n",
    "\n",
    "random.seed(52)\n",
    "selected_questions = random.choices(questions, k=5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "> where do frankenstein and the monster first meet\n",
      " Victor retreats into the mountains, and that is where the Creature finds him and pleads for Victor to hear his tale.\n",
      "\n",
      "> who are the actors in fast and furious\n",
      " The main cast of Fast & Furious includes Vin Diesel as Dominic Toretto, Paul Walker as Brian O'Conner, Michelle Rodriguez as Letty Ortiz, Jordana Brewster as Mia Toretto, Tyrese Gibson as Roman Pearce, and Ludacris as Tej Parker.\n",
      "\n",
      "> properties of red black tree in data structure\n",
      " The properties of a red-black tree in data structure are that each node is either red or black, the root is black, all leaves (NIL) are black, and if a node is red, then both its children are black. Additionally, every path from a given node to any of its descendant NIL nodes contains the same number of black nodes.\n",
      "\n",
      "> who designed the national coat of arms of south africa\n",
      " Iaan Bekker\n",
      "\n",
      "> caravaggio's death of the virgin pamela askew\n",
      " I don't know.\n",
      "\n"
     ]
    }
   ],
   "source": [
    "for question in selected_questions:\n",
    "    print(\">\", question)\n",
    "    print(qa.run(question), end=\"\\n\\n\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Custom prompt templates\n",
    "\n",
    "The `stuff` chain type in Langchain uses a specific prompt with question and context documents incorporated. This is what the default prompt looks like:\n",
    "\n",
    "```text\n",
    "Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.\n",
    "{context}\n",
    "Question: {question}\n",
    "Helpful Answer:\n",
    "```\n",
    "\n",
    "We can, however, provide our prompt template and change the behaviour of the OpenAI LLM, while still using the `stuff` chain type. It is important to keep `{context}` and `{question}` as placeholders.\n",
    "\n",
    "#### Experimenting with custom prompts\n",
    "\n",
    "We can try using a different prompt template, so the model:\n",
    "1. Responds with a single-sentence answer if it knows it.\n",
    "2. Suggests a random song title if it doesn't know the answer to our question."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.prompts import PromptTemplate\n",
    "custom_prompt = \"\"\"\n",
    "Use the following pieces of context to answer the question at the end. Please provide\n",
    "a short single-sentence summary answer only. If you don't know the answer or if it's\n",
    "not present in given context, don't try to make up an answer, but suggest me a random\n",
    "unrelated song title I could listen to.\n",
    "Context: {context}\n",
    "Question: {question}\n",
    "Helpful Answer:\n",
    "\"\"\"\n",
    "\n",
    "custom_prompt_template = PromptTemplate(\n",
    "    template=custom_prompt, input_variables=[\"context\", \"question\"]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "custom_qa = VectorDBQA.from_chain_type(\n",
    "    llm=llm,\n",
    "    chain_type=\"stuff\",\n",
    "    vectorstore=doc_store,\n",
    "    return_source_documents=False,\n",
    "    chain_type_kwargs={\"prompt\": custom_prompt_template},\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "> what was uncle jesse's original last name on full house\n",
      "Uncle Jesse's original last name on Full House was Cochran.\n",
      "\n",
      "> when did the volcano erupt in indonesia 2018\n",
      "No information about a volcano erupting in Indonesia in 2018 is present in the given context. Suggested song title: \"Volcano\" by U2.\n",
      "\n",
      "> what does a dualist way of thinking mean\n",
      "A dualist way of thinking means believing that humans possess a non-physical mind or soul which is distinct from their physical body.\n",
      "\n",
      "> the first civil service commission in india was set up on the basis of recommendation of\n",
      "The first Civil Service Commission in India was not set up on the basis of a recommendation.\n",
      "\n",
      "> how old do you have to be to get a tattoo in utah\n",
      "In Utah, you must be at least 18 years old to get a tattoo.\n",
      "\n"
     ]
    }
   ],
   "source": [
    "random.seed(41)\n",
    "for question in random.choices(questions, k=5):\n",
    "    print(\">\", question)\n",
    "    print(custom_qa.run(question), end=\"\\n\\n\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
